﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>public class ViewModel : INotifyPropertyChanged { public string Name { get; set; } ... }</example>
    [ExportCodeRefactoringProvider("ImplementPropertyChangeNotification", LanguageNames.CSharp)]
    internal partial class ImplementPropertyChangeNotificationCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);

            var property = root.FindToken(span.Start).Parent.FirstAncestorOrSelf<PropertyDeclarationSyntax>();
            if (property == null || property.Modifiers.Any(SyntaxKind.StaticKeyword))
                return null;

            var type = property.Ancestors().OfType<TypeDeclarationSyntax>().FirstOrDefault();
            var model = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
            var typeInfo = model.GetDeclaredSymbol(type, cancellationToken) as ITypeSymbol;

            var notifyPropertyChangedInterface = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanged).FullName);

            // if declaring type implements INotifyPropertyChanged
            if (typeInfo.AllInterfaces.Any(i => i.Equals(notifyPropertyChangedInterface)) &&
                property.AccessorList.Accessors.Any(a => a.IsKind(SyntaxKind.GetAccessorDeclaration) && a.Body == null) && property.AccessorList.Accessors.Any(a => a.IsKind(SyntaxKind.SetAccessorDeclaration) && a.Body == null)
            )
            {
                // detect wether the declaring type implements INotifyPropertyChanging
                var notifyPropertyChangingInterface = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanging).FullName);
                bool implementChanging = typeInfo.AllInterfaces.Any(i => i.Equals(notifyPropertyChangingInterface));

                return new[] {
                    CodeAction.Create("Implement property change notification", c => ImplementNotifyPropertyChangedAsync(root, document, property, model, implementChanging, c)),
                    CodeAction.Create("Implement property change notification (using compiler services)", c => ImplementNotifyPropertyChangedUsingCompilerServicesAsync(root, document, property, model, document.Project.Solution.Workspace, implementChanging, c))
                };
            }
            
            return null;
        }

        protected virtual async Task<Document> ImplementNotifyPropertyChangedAsync(
            SyntaxNode root, Document document,
            PropertyDeclarationSyntax property, SemanticModel model,
            bool implementChanging,
            CancellationToken c
        )
        {
            // compute the name of the backing field
            string fieldName = GenerateBackingFieldName(property, model, c);

            // find declaring type
            TypeDeclarationSyntax type = property.FirstAncestorOrSelf<TypeDeclarationSyntax>();

            // add backing field to type
            TypeDeclarationSyntax newType = type
                .WithBackingField(model.GetTypeInfo(property.Type, c).Type, fieldName, document.Project.Solution.Workspace);

            // add property changed event
            ITypeSymbol typeSymbol = model.GetDeclaredSymbol(type);
            ITypeSymbol notifyPropertyChanged = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanged).FullName);
            IEventSymbol eventSymbol = notifyPropertyChanged.GetMembers("PropertyChanged").Single() as IEventSymbol;

            if (typeSymbol.FindImplementationForInterfaceMember(eventSymbol) == null)
            {
                newType = CodeGenerator.AddEventDeclaration(
                    newType,
                    CodeGenerationSymbolFactory.CreateEventSymbol(
                        null,
                        Accessibility.Public,
                        default(SymbolModifiers),
                        eventSymbol.Type,
                        null,
                        eventSymbol.Name
                    ),
                    document.Project.Solution.Workspace
                );
            }

            if (implementChanging)
            {
                ITypeSymbol notifyPropertyChanging = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanging).FullName);
                eventSymbol = notifyPropertyChanging.GetMembers("PropertyChanging").Single() as IEventSymbol;

                if (typeSymbol.FindImplementationForInterfaceMember(eventSymbol) == null)
                {
                    newType = CodeGenerator.AddEventDeclaration(
                        newType,
                        CodeGenerationSymbolFactory.CreateEventSymbol(
                            null,
                            Accessibility.Public,
                            default(SymbolModifiers),
                            eventSymbol.Type,
                            null,
                            eventSymbol.Name
                        ),
                        document.Project.Solution.Workspace
                    );
                }
            }

            // find property declaration in new type
            property = newType.Members.OfType<PropertyDeclarationSyntax>().Single(m => property.IsEquivalentTo(m));

            AccessorDeclarationSyntax
                getter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)),
                setter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.SetAccessorDeclaration));

            BlockSyntax body = SyntaxFactory.Block(
                SyntaxFactory.ExpressionStatement(
                    SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                        SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                SyntaxFactory.ThisExpression(),
                                SyntaxFactory.IdentifierName(fieldName)
                        )
                            .WithAdditionalAnnotations(Simplifier.Annotation),
                        SyntaxFactory.IdentifierName("value")
                    )
                )
                    .WithTrailingTrivia(
                        SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                    ),
                SyntaxFactory.IfStatement(
                    condition: SyntaxFactory.BinaryExpression(SyntaxKind.NotEqualsExpression,
                        SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                            SyntaxFactory.ThisExpression(),
                            SyntaxFactory.IdentifierName("PropertyChanged")
                        )
                            .WithAdditionalAnnotations(Simplifier.Annotation),
                        SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                    ),
                    statement: SyntaxFactory.ExpressionStatement(
                        SyntaxFactory.InvocationExpression(
                            SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                SyntaxFactory.ThisExpression(),
                                SyntaxFactory.IdentifierName("PropertyChanged")
                            )
                                .WithAdditionalAnnotations(Simplifier.Annotation),
                            SyntaxFactory.ArgumentList(
                                SyntaxFactory.SeparatedList(new[] {
                                    SyntaxFactory.Argument(SyntaxFactory.ThisExpression()),
                                        SyntaxFactory.Argument(
                                            SyntaxFactory.ObjectCreationExpression(
                                                SyntaxFactory.ParseTypeName(model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.PropertyChangedEventArgs).FullName).ToDisplayString()),
                                                SyntaxFactory.ArgumentList(
                                                    SyntaxFactory.SeparatedList(new [] {
                                                        SyntaxFactory.Argument(
                                                            SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(property.Identifier.ValueText))
                                                        )
                                                    })
                                                ),
                                                default(InitializerExpressionSyntax)
                                            )
                                        )
                                })
                            )
                        )
                    )
                )
                    .WithLeadingTrivia(
                        SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine)
                    )
            );

            if (implementChanging)
                body = body.WithStatements(
                    body.Statements.Insert(0,
                        SyntaxFactory.IfStatement(
                            condition: SyntaxFactory.BinaryExpression(SyntaxKind.NotEqualsExpression,
                                SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                    SyntaxFactory.ThisExpression(),
                                    SyntaxFactory.IdentifierName("PropertyChanging")
                                )
                                    .WithAdditionalAnnotations(Simplifier.Annotation),
                                SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression)
                            ),
                            statement: SyntaxFactory.ExpressionStatement(
                                SyntaxFactory.InvocationExpression(
                                    SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                        SyntaxFactory.ThisExpression(),
                                        SyntaxFactory.IdentifierName("PropertyChanging")
                                    )
                                        .WithAdditionalAnnotations(Simplifier.Annotation),
                                    SyntaxFactory.ArgumentList(
                                        SyntaxFactory.SeparatedList(new[] {
                                            SyntaxFactory.Argument(SyntaxFactory.ThisExpression()),
                                                SyntaxFactory.Argument(
                                                    SyntaxFactory.ObjectCreationExpression(
                                                        SyntaxFactory.ParseTypeName(model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.PropertyChangingEventArgs).FullName).ToDisplayString()),
                                                        SyntaxFactory.ArgumentList(
                                                            SyntaxFactory.SeparatedList(new [] {
                                                                SyntaxFactory.Argument(
                                                                    SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(property.Identifier.ValueText))
                                                                )
                                                            })
                                                        ),
                                                        default(InitializerExpressionSyntax)
                                                    )
                                                )
                                        })
                                    )
                                )
                            )
                        )
                            .WithLeadingTrivia(
                                SyntaxFactory.SyntaxTrivia(SyntaxKind.EndOfLineTrivia, Environment.NewLine) // BUG: it has no effect
                            )
                    )
                );

            // expand accessors
            var newNode = property
                .WithAccessorList(
                    SyntaxFactory.AccessorList(
                        new SyntaxList<AccessorDeclarationSyntax>()
                            .Add(
                                getter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.ReturnStatement(
                                            SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                                SyntaxFactory.ThisExpression(),
                                                SyntaxFactory.IdentifierName(fieldName)
                                            )
                                                .WithAdditionalAnnotations(Simplifier.Annotation)
                                        )
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                                    .WithTrailingTrivia(default(SyntaxTrivia))
                            )
                            .Add(
                                setter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.IfStatement(
                                            condition: SyntaxFactory.BinaryExpression(SyntaxKind.NotEqualsExpression,
                                                SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                                    SyntaxFactory.ThisExpression(),
                                                    SyntaxFactory.IdentifierName(fieldName)
                                                )
                                                    .WithAdditionalAnnotations(Simplifier.Annotation),
                                                SyntaxFactory.IdentifierName("value")
                                            ),
                                            statement: body
                                        )
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                            )
                    )
                )
                .WithAdditionalAnnotations(Formatter.Annotation)
                .WithAdditionalAnnotations(Simplifier.Annotation)
            ;

            // replace nodes
            newType = newType.ReplaceNode(property, newNode);

            var newRoot = root
                .ReplaceNode(type, newType)
            ;

            return document.WithSyntaxRoot(newRoot);
        }

        protected virtual async Task<Document> ImplementNotifyPropertyChangedUsingCompilerServicesAsync(
            SyntaxNode root, Document document, PropertyDeclarationSyntax property, SemanticModel model, Workspace workspace, bool implementChanging, CancellationToken c
        )
        {
            // compute the name of the backing field
            string fieldName = GenerateBackingFieldName(property, model, c);

            // find declaring type
            TypeDeclarationSyntax type = property.Ancestors().OfType<TypeDeclarationSyntax>().First();

            // add backing field to type
            TypeDeclarationSyntax newType = type
                .WithBackingField(model.GetTypeInfo(property.Type, c).Type, fieldName, document.Project.Solution.Workspace)
                .WithSetPropertyMethod(type, model, implementChanging, workspace)
            ;

            // add property changed event
            ITypeSymbol typeSymbol = model.GetDeclaredSymbol(type);
            ITypeSymbol notifyPropertyChanged = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanged).FullName);
            IEventSymbol eventSymbol = notifyPropertyChanged.GetMembers("PropertyChanged").Single() as IEventSymbol;

            if (typeSymbol.FindImplementationForInterfaceMember(eventSymbol) == null)
            {
                newType = CodeGenerator.AddEventDeclaration(
                    newType,
                    CodeGenerationSymbolFactory.CreateEventSymbol(
                        null,
                        Accessibility.Public,
                        default(SymbolModifiers),
                        eventSymbol.Type,
                        null,
                        eventSymbol.Name
                    ),
                    document.Project.Solution.Workspace
                );
            }

            if (implementChanging)
            {
                ITypeSymbol notifyPropertyChanging = model.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanging).FullName);
                eventSymbol = notifyPropertyChanging.GetMembers("PropertyChanging").Single() as IEventSymbol;

                if (typeSymbol.FindImplementationForInterfaceMember(eventSymbol) == null)
                {
                    newType = CodeGenerator.AddEventDeclaration(
                        newType,
                        CodeGenerationSymbolFactory.CreateEventSymbol(
                            null,
                            Accessibility.Public,
                            default(SymbolModifiers),
                            eventSymbol.Type,
                            null,
                            eventSymbol.Name
                        ),
                        document.Project.Solution.Workspace
                    );
                }
            }

            // find property declaration in new type
            property = newType.Members.OfType<PropertyDeclarationSyntax>().Single(m => property.IsEquivalentTo(m));

            AccessorDeclarationSyntax
                getter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)),
                setter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.SetAccessorDeclaration));

            // expand accessors
            var newNode = property
                .WithAccessorList(
                    SyntaxFactory.AccessorList(
                        new SyntaxList<AccessorDeclarationSyntax>()
                            .Add(
                                getter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.ReturnStatement(
                                            SyntaxFactory.IdentifierName(fieldName)
                                        )
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                                    .WithTrailingTrivia(default(SyntaxTrivia))
                            )
                            .Add(
                                setter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.ParseStatement(string.Format("this.SetProperty(ref {0}, value);", fieldName, property.Identifier.ValueText))
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                            )
                    )
                )
                .WithAdditionalAnnotations(Formatter.Annotation)
            ;

            // replace nodes
            newType = newType.ReplaceNode(property, newNode);

            var newRoot = root
                .ReplaceNode(type, newType)
            ;

            return document.WithSyntaxRoot(newRoot);
        }

        private static string GenerateBackingFieldName(PropertyDeclarationSyntax property, SemanticModel semanticModel, CancellationToken c)
        {
            // backing field name
            var baseName = property.Identifier.ValueText;
            baseName = string.Concat('_', char.ToLower(baseName[0]), baseName.Substring(1));

            var propertySymbol = semanticModel.GetDeclaredSymbol(property, c);
            if (propertySymbol == null ||
                propertySymbol.ContainingType == null)
                return baseName;

            // generate new name with index
            var index = 0;
            var name = baseName;

            while (propertySymbol.ContainingType.MemberNames.Contains(name))
                name = baseName + ++index;

            return name;
        }
    }

    internal static class NotifyExtensions
    {
        private static IMethodSymbol FindSetPropertyMethod(INamedTypeSymbol classSymbol, Compilation compilation)
        {
            var stringType = compilation.GetSpecialType(SpecialType.System_String);

            foreach (IMethodSymbol method in classSymbol
                .GetMembers("SetProperty").Union(classSymbol.GetMembers("RaisePropertyChanged"))
                .OfType<IMethodSymbol>()
                .Where(m => m.TypeParameters.Length == 1 && m.Parameters.Length == 3)
            )
            {
                var parameters = method.Parameters;
                var typeParameter = method.TypeParameters[0];

                if (method.ReturnsVoid &&
                    parameters[0].RefKind == RefKind.Ref &&
                    parameters[0].Type.Equals(typeParameter) &&
                    parameters[1].Type.Equals(typeParameter) &&
                    parameters[2].Type.Equals(stringType)
                )
                    return method;
            }

            return null;
        }

        public static TypeDeclarationSyntax WithSetPropertyMethod(this TypeDeclarationSyntax node, TypeDeclarationSyntax original, SemanticModel semanticModel, bool implementChanging, Workspace workspace)
        {
            var classSymbol = semanticModel.GetDeclaredSymbol(original);
            var interfaceSymbol = semanticModel.Compilation.GetTypeByMetadataName(typeof(System.ComponentModel.INotifyPropertyChanged).FullName);

            var setPropertyMethod = FindSetPropertyMethod(classSymbol, semanticModel.Compilation);
            if (setPropertyMethod == null)
            {
                node = CodeGenerator.AddMethodDeclaration(
                    node,
                    GenerateSetPropertyMethod(implementChanging, semanticModel.Compilation),
                    workspace
                );
            }

            return node;
        }

        internal static IMethodSymbol GenerateSetPropertyMethod(bool implementChanging, Compilation compilation)
        {
            var body = SyntaxFactory.ParseStatement(
                implementChanging
                    ?
@"if (!System.Collections.Generic.EqualityComparer<T>.Default.Equals(field, value))
{
    var propertyChangingHandler = this.PropertyChanging;
    if (propertyChangingHandler != null)
        propertyChangingHandler(this, new System.ComponentModel.PropertyChangingEventArgs(name));

    field = value;

    var propertyChangedHandler = this.PropertyChanged;
    if (propertyChangedHandler != null)
        propertyChangedHandler(this, new System.ComponentModel.PropertyChangedEventArgs(name));
}"
                    :
@"if (!System.Collections.Generic.EqualityComparer<T>.Default.Equals(field, value))
{
    field = value;

    var propertyChangedHandler = this.PropertyChanged;
    if (propertyChangedHandler != null)
        propertyChangedHandler(this, new System.ComponentModel.PropertyChangedEventArgs(name));
}"
            );

            body = body.WithAdditionalAnnotations(Simplifier.Annotation);

            var stringType = compilation.GetSpecialType(SpecialType.System_String);
            var voidType = compilation.GetSpecialType(SpecialType.System_Void);

            var typeParameter = CodeGenerationSymbolFactory.CreateTypeParameterSymbol("T");

            return CodeGenerationSymbolFactory.CreateMethodSymbol(
                attributes: null,
                accessibility: Accessibility.Protected,
                modifiers: new SymbolModifiers(),
                returnType: voidType,
                explicitInterfaceSymbol: null,
                name: "SetProperty", // TODO: Handle if declaration mathes another method
                typeParameters: new[] { typeParameter },
                parameters: new[] {
                    CodeGenerationSymbolFactory.CreateParameterSymbol(
                        attributes: null,
                        refKind: RefKind.Ref,
                        isParams: false,
                        type: typeParameter,
                        name: "field"
                    ),
                    CodeGenerationSymbolFactory.CreateParameterSymbol(typeParameter, "value"),
                    CodeGenerationSymbolFactory.CreateParameterSymbol(
                        new List<AttributeData>
                        {
                            CodeGenerationSymbolFactory.CreateAttributeData(
                                compilation.GetTypeByMetadataName(typeof(System.Runtime.CompilerServices.CallerMemberNameAttribute).FullName)
                            )
                        },
                        RefKind.None,
                        false,
                        stringType,
                        "name",
                        true,
                        true,
                        defaultValue: null
                    )
                },
                statements: new[] { body }
            );
        }
    }
}
